﻿
using System;
using System.Collections.Generic;
using Liang.Util.Task.Enums;

namespace Liang.Util.Task
{
	public class TimeCalculations
	{

		/// <summary>
		/// 时间误差
		/// </summary>
		public static TimeSpan Deviation = new TimeSpan(200000);

		#region 公共成员方法
		/// <summary>
		/// 策略变更
		/// </summary>
		/// <param name="config"></param>
		public static void CheckStrategy(TimerStrategy config)
		{
			var Config = config;
			if (Config == null || Config.StrategyByDay == null) throw new Exception("定时器时间配置异常！");

			if (Config.Days != null) Config.Days = TimerStrategyManager.SortAndDistinct(Config.Days);
			if (Config.Months != null) Config.Months = TimerStrategyManager.SortAndDistinct(Config.Months);

			switch (Config.TimerMode)
			{
				case TimerStrategyMode.Date:
					if (Config.Dates == null || Config.Dates.Count == 0) throw new Exception("定时器时间配置异常Dates(取值不能为空)！");
					for (var i = 0; i < Config.Dates.Count; i++)
					{
						var d = Config.Dates[i];
						Config.Dates[i] = new DateTime(4, d.Month, d.Day, d.Hour, d.Minute, d.Second);
					}
					if (Config.Dates != null) Config.Dates = TimerStrategyManager.SortAndDistinct(Config.Dates);
					return;
				case TimerStrategyMode.EveryDay: return;
				case TimerStrategyMode.EveryMonth:
				case TimerStrategyMode.Month:
					if (Config.Days == null || Config.Days.Count == 0) throw new Exception("定时器时间配置异常Days(取值不能为空)！");
					foreach (var d in Config.Days) if (d < 1 || d > 31) throw new Exception("定时器时间配置异常(天数取值只能是1~31)！");
					if (Config.TimerMode == TimerStrategyMode.Month)
					{
						if (Config.Months == null || Config.Months.Count == 0) throw new Exception("定时器时间配置异常Months(取值不能为空)！");
						foreach (var m in Config.Months) if (m < 1 || m > 12) throw new Exception("定时器时间配置异常(天数取值只能是1~12)！");
					}
					break;
				case TimerStrategyMode.EveryWeek:
					if (Config.Days == null || Config.Days.Count == 0) throw new Exception("定时器时间配置异常Days(取值不能为空)！");
					foreach (var d in Config.Days) if (d < 0 || d > 6) throw new Exception("定时器时间配置异常Days(星期取值只能是0~6)！");
					break;
				case TimerStrategyMode.LastDayOfMonth:
					if (Config.Days == null || Config.Days.Count == 0) Config.Days = new List<int>(new int[] { 1 });
					foreach (var d in Config.Days) if (d < 1 || d > 28) throw new Exception("定时器时间配置异常(倒数的天数只能是1~28，即倒数的第1天，第2天。。。有些月份并没有29.30.31天,因此最大只允许倒数第28天)！");
					break;
				case TimerStrategyMode.EveryYear:
					if (Config.Days == null || Config.Days.Count == 0) throw new Exception("定时器时间配置异常Days(取值不能为空)！");
					foreach (var d in Config.Days) if (d < 1 || d > 366) throw new Exception("定时器时间配置异常(天数取值只能是1~366)！");
					break;
			}
		}

		/// <summary>
		/// 判断时间是否到了
		/// </summary>
		/// <param name="dayStrategy"></param>
		/// <returns>true时间已经到了，false时间还未到</returns>
		public static bool TimeIsUp(TimerStrategy strategy)
		{
			DateTime dt = DateTime.Now;
			//dt = TestDateTime;
			if (CheckTimeIsUp(strategy.StrategyByDay, dt.TimeOfDay))
			{
				switch (strategy.TimerMode)
				{
					case TimerStrategyMode.Date: return strategy.Dates.Contains(new DateTime(4, dt.Month, dt.Day));
					case TimerStrategyMode.EveryDay: return true;
					case TimerStrategyMode.EveryMonth: return strategy.Days.Contains(dt.Day);
					case TimerStrategyMode.Month: return strategy.Months.Contains(dt.Month) && strategy.Days.Contains(dt.Day);
					case TimerStrategyMode.LastDayOfMonth: return strategy.Days.Contains(GetLastDayByMonth(dt.Month, DateTime.IsLeapYear(dt.Year)) - dt.Day + 1);
					case TimerStrategyMode.EveryWeek: return strategy.Days.Contains((int)dt.DayOfWeek);
					case TimerStrategyMode.EveryYear: return strategy.Days.Contains(dt.DayOfYear);
					default: return false;
				}
			}
			else
				return false;
		}

		/// <summary>
		/// 时间是否到了
		/// </summary>
		/// <param name="dayStrategy"></param>
		/// <param name="time"></param>
		/// <returns></returns>
		private static bool CheckTimeIsUp(TimerStrategyByDay dayStrategy, TimeSpan time)
		{
			time = time.Add(Deviation);
			var tmp = new TimeSpan(time.Hours, time.Minutes, time.Seconds);

			if (dayStrategy.TimePoints == null)
				return (tmp.Ticks == 0);
			else
			{
				foreach (var t in dayStrategy.TimePoints)
				{
					if (t == tmp) return true;
				}
				return false;
			}
		}

		/// <summary>
		/// 从现在起到下次时间到还有多少时间
		/// </summary>
		/// <param name="dayStrategy"></param>
		/// <param name="timerMode"></param>
		/// <returns>返回时间间隔</returns>
		public static TimeSpan GetNextTimeUp(TimerStrategy strategy)
		{
			///目标时间
			DateTime _NextDateTime = GetNextDateTime(strategy);	// 保存下一次要执行的时间
			return _NextDateTime - DateTime.Now;
		}

		/// <summary>
		/// 获取下一次指定配置的时间是多少
		/// </summary>
		/// <returns></returns>
		public static DateTime GetNextDateTime(TimerStrategy strategy)
		{
			DateTime dt = DateTime.Now;
			//dt = TestDateTime;

			TimeSpan MinTimeByDay;
			bool hasExpectDate;
			TimeSpan time = GetNextTimeConfig(strategy.StrategyByDay, dt, out MinTimeByDay, out hasExpectDate);
			int targetday = -1, nextTargetDay = -1;
			var dtIsLeapYear = DateTime.IsLeapYear(dt.Year);
			var Config = strategy;
			DateTime now, target;

			switch (strategy.TimerMode)
			{
				case TimerStrategyMode.EveryDay:

					#region (Checked)每天指定某时执行一次
					now = new DateTime(1, 1, 1, dt.Hour, dt.Minute, dt.Second);
					target = new DateTime(1, 1, 1, time.Hours, time.Minutes, time.Seconds);
					if (now.Ticks >= target.Ticks) dt = dt.AddDays(1.0);	//如果当前时间小于指定时刻，则不需要加天

					dt = new DateTime(dt.Year, dt.Month, dt.Day, time.Hours, time.Minutes, time.Seconds);
					#endregion

					break;
				case TimerStrategyMode.EveryMonth:

					#region (Checked)每月指定的那些天某时执行一次
					dt = GetTargetDateTime(dt, Config.Days, MinTimeByDay, time, hasExpectDate);
					#endregion

					break;
				case TimerStrategyMode.LastDayOfMonth:

					#region (Checked)每个月倒数第N天的某时某刻执行一次
					now = new DateTime(1, 1, dt.Day, dt.Hour, dt.Minute, dt.Second);

					var currday = -1;
					var lasdayByMonth = GetLastDayByMonth(dt.Month, DateTime.IsLeapYear(dt.Year));
					if (GetNextValueByDesc(dt.Day, lasdayByMonth, Config.Days, out currday, out nextTargetDay))
						targetday = lasdayByMonth - (currday - 1);
					else
						targetday = lasdayByMonth - (nextTargetDay - 1);

					if (targetday != dt.Day)	// 天数变更了.即,不再是同一天的不同时间定时了. 那么时间就应该变更成一天多个时间中的最小时间
						time = MinTimeByDay;

					target = new DateTime(1, 1, targetday, time.Hours, time.Minutes, time.Seconds);	// 1月有31天，所以可以接受任何合法的Day值（因为在赋值时已判断1~31）
					if (now.Ticks >= target.Ticks)
					{
						if (dt.Day > lasdayByMonth - (nextTargetDay - 1))// 如果下一个天数比当前小，则加一个月（因为这里的数字是代表倒数第几，所以字面越大反而越小）
						{
							dt = dt.AddMonths(1);
							lasdayByMonth = GetLastDayByMonth(dt.Month, DateTime.IsLeapYear(dt.Year));
						}
						targetday = lasdayByMonth - (nextTargetDay - 1);
						dt = new DateTime(dt.Year, dt.Month, targetday, time.Hours, time.Minutes, time.Seconds);// 根据新月份求新月份的最后一天。
					}
					else
						dt = new DateTime(dt.Year, dt.Month, targetday, time.Hours, time.Minutes, time.Seconds);
					#endregion

					break;
				case TimerStrategyMode.EveryWeek:

					#region (Checked)每星期指定星期某时执行一次
					dt = GetTargetDateTimeByEveryWeek(dt, Config.Days, MinTimeByDay, time, hasExpectDate);
					#endregion

					break;
				case TimerStrategyMode.Date:

					#region (Checked)每年指定哪些月-日执行
					dt = GetTargetDateTime(dt, Config.Dates, MinTimeByDay, time, hasExpectDate);
					#endregion

					break;
				case TimerStrategyMode.Month:

					#region (Checked)指定几个月的哪些天哪些时间可以执行
					if (Config.Dates == null || Config.Dates.Count == 0)
					{
						var dates = GetDateSettings(Config.Months, Config.Days);
						Config.Dates.AddRange(dates);
					}
					dt = GetTargetDateTime(dt, Config.Dates, MinTimeByDay, time, hasExpectDate);
					#endregion

					break;
				case TimerStrategyMode.EveryYear:

					#region (checked)每年指定哪些天某时执行
					dt = GetTargetDateTimeByEveryYear(dt, Config.Days, MinTimeByDay, time, hasExpectDate);
					#endregion

					break;
				default:
					throw new Exception("定时器时间配置异常！");
			}

			return dt;
		}

		#region 目标日期计算
		/// <summary>
		/// 获取下一个执行日期的时间(每星期中的N天)
		/// </summary>
		/// <param name="currDate">当前日期的时间</param>
		/// <param name="days">执行的天数</param>
		/// <param name="minTime">定时的最小时间</param>
		/// <param name="expectTime">期望定时的时间</param>
		/// <param name="hasExpectDate">是否找到期望的定时时间</param>
		/// <returns></returns>
		private static DateTime GetTargetDateTimeByEveryYear(DateTime currDate, List<int> days, TimeSpan minTime, TimeSpan expectTime, bool hasExpectDate)
		{
			var currDay = currDate.DayOfYear;
			var isLeapYear = DateTime.IsLeapYear(currDate.Year);
			var maxDay = isLeapYear ? 366 : 365;

			var target = int.MinValue;
			var targetTime = minTime;

			#region 获取当前年份的下个天数
			foreach (var d in days)
			{
				if (d == currDay && hasExpectDate)
				{
					target = currDay;
					targetTime = expectTime;
					break;
				}
				else if (d > currDay && d <= maxDay)
				{
					target = d;
					targetTime = minTime;
					break;
				}
			}
			#endregion

			#region 获取下个年份的第一个天数
			if (target == int.MinValue)
			{
				target = days[0];
				currDate = currDate.AddYears(1);
				if (target == 366)
					for (int i = 0; i < 8; i++)
					{
						if (DateTime.IsLeapYear(currDate.Year + i))
						{
							currDate = currDate.AddYears(i);
							break;
						}
					}
			}
			#endregion

			currDate = new DateTime(currDate.Year, 1, 1, targetTime.Hours, targetTime.Minutes, targetTime.Seconds);
			currDate = currDate.AddDays(target - 1);

			return currDate;
		}

		/// <summary>
		/// 获取下一个执行日期的时间(每星期中的N天)
		/// </summary>
		/// <param name="currDate">当前日期的时间</param>
		/// <param name="days">执行的天数</param>
		/// <param name="minTime">定时的最小时间</param>
		/// <param name="expectTime">期望定时的时间</param>
		/// <param name="hasExpectDate">是否找到期望的定时时间</param>
		/// <returns></returns>
		private static DateTime GetTargetDateTimeByEveryWeek(DateTime currDate, List<int> days, TimeSpan minTime, TimeSpan expectTime, bool hasExpectDate)
		{
			var currDay = (int)currDate.DayOfWeek;

			var target = int.MinValue;
			var targetTime = minTime;

			#region 获取当前星期的下个星期数
			foreach (var d in days)
			{
				if (d == currDay && hasExpectDate)
				{
					target = currDay;
					targetTime = expectTime;
					break;
				}
				else if (d > currDay)
				{
					target = d;
					targetTime = minTime;
					break;
				}
			}
			#endregion

			#region 获取下个星份的第一个星期数
			if (target == int.MinValue)
			{
				target = days[0];
				currDate = currDate.AddDays(7);
			}

			currDate = currDate.AddDays(target - currDay);
			#endregion

			return new DateTime(currDate.Year, currDate.Month, currDate.Day, targetTime.Hours, targetTime.Minutes, targetTime.Seconds);
		}

		/// <summary>
		/// 获取下一个执行日期的时间(每月中的N天)
		/// </summary>
		/// <param name="currDate">当前日期的时间</param>
		/// <param name="days">执行的天数</param>
		/// <param name="minTime">定时的最小时间</param>
		/// <param name="expectTime">期望定时的时间</param>
		/// <param name="hasExpectDate">是否找到期望的定时时间</param>
		/// <returns></returns>
		private static DateTime GetTargetDateTime(DateTime currDate, List<int> days, TimeSpan minTime, TimeSpan expectTime, bool hasExpectDate)
		{
			var currDay = currDate.Day;
			var isLeapYear = DateTime.IsLeapYear(currDate.Year);
			var maxDay = GetLastDayByMonth(currDate.Month, isLeapYear);

			var target = int.MinValue;
			var targetTime = minTime;

			#region 获取当前月份的下个日期
			foreach (var d in days)
			{
				if (d == currDay && hasExpectDate)
				{
					target = currDay;
					targetTime = expectTime;
					break;
				}
				else if (d > currDay && d <= maxDay)
				{
					target = d;
					targetTime = minTime;
					break;
				}
			}
			#endregion

			#region 获取下个月份的第一个日期
			if (target == int.MinValue)
			{
				target = days[0];
				currDate = currDate.AddMonths(1);
				for (int i = 0; i < 2; i++)
				{
					if (target <= GetLastDayByMonth(currDate.Month + i, DateTime.IsLeapYear(currDate.AddMonths(i).Year)))
					{
						currDate = currDate.AddMonths(i);
						break;
					}
				}
			}
			#endregion

			return new DateTime(currDate.Year, currDate.Month, target, targetTime.Hours, targetTime.Minutes, targetTime.Seconds);
		}

		/// <summary>
		/// 获取下一个执行日期的时间(每年中的N日期)
		/// </summary>
		/// <param name="currDate">当前日期的时间</param>
		/// <param name="dates">日期配置</param>
		/// <param name="minTime">定时的最小时间</param>
		/// <param name="expectTime">期望定时的时间</param>
		/// <param name="hasExpectDate">是否找到期望的定时时间</param>
		/// <returns></returns>
		private static DateTime GetTargetDateTime(DateTime currDate, List<DateTime> dates, TimeSpan minTime, TimeSpan expectTime, bool hasExpectDate)
		{
			var currVal = new DateTime(4, currDate.Month, currDate.Day);
			var target = default(DateTime);
			var targetTime = minTime;
			var targetYear = currDate.Year;
			var isLeapYear = DateTime.IsLeapYear(targetYear);

			#region 获取当前年份的下个日期
			foreach (var val in dates)
			{
				if (val == currVal && hasExpectDate)
				{
					target = currDate;
					targetTime = expectTime;
					break;
				}
				else if (val > currVal && (isLeapYear || !(val.Month == 2 && val.Day == 29)))
				{
					target = val;
					targetTime = minTime;
					break;
				}
			}
			#endregion

			#region 获取下个年份的第一个日期
			if (target == default(DateTime))
			{
				target = dates[0];
				targetYear += 1;	// 没有找到当前年份之后的日期,因此只能年份加1年.
				if (dates[0].Month == 2 && dates[0].Day == 29)// 如果当前是2月29号，则需要特殊处理
				{
					if (!DateTime.IsLeapYear(targetYear) && dates.Count > 1)// 如果明年不是闰年, 且日期配置又不只一项,这样就可以取第二个日期.
						target = dates[1];
					else if (dates.Count == 1)
						for (int i = 0; i < 8; i++)		// 如果日期配置只有一项,那么只能选择一个闰年年份
							if (DateTime.IsLeapYear(targetYear + i))
								targetYear += i;
				}
			}
			#endregion

			return new DateTime(targetYear, target.Month, target.Day, targetTime.Hours, targetTime.Minutes, targetTime.Seconds);
		}

		/// <summary>
		/// 组合相关的日期和时间
		/// </summary>
		/// <param name="months"></param>
		/// <param name="days"></param>
		/// <returns></returns>
		private static List<DateTime> GetDateSettings(List<int> months, List<int> days)
		{
			var dates = new List<DateTime>();
			var maxDay = 0;
			foreach (var m in months)
			{
				maxDay = GetLastDayByMonth(m, true);
				foreach (var d in days)
				{
					if (d > maxDay) break;
					dates.Add(new DateTime(4, m, d));
				}
			}

			dates.TrimExcess();
			return dates;
		}

		#region 每个月倒数第几天运行
		/// <summary>
		/// 获取下一次要执行时间的天
		/// </summary>
		/// <typeparam name="T">类型</typeparam>
		/// <param name="day">当前天</param>
		/// <param name="lastDay">当前月份的最后一天</param>
		/// <param name="values">值列表</param>
		/// <param name="currVal">当前值</param>
		/// <param name="nextVal">下一个值</param>
		/// <returns>当前值列表中是否有包含当前值</returns>
		private static bool GetNextValueByDesc(int day, int lastDay, List<int> values, out int currVal, out int nextVal)
		{
			var has = false;
			currVal = lastDay - (day - 1);
			for (int i = values.Count - 1; i >= 0; i--)
			{
				if (lastDay - (values[i] - 1) == day)
				{
					currVal = values[i];
					has = true;
				}
				else if (lastDay - (values[i] - 1) > day)
				{// 找到第一个比给定值小的立即返回（因为值列表是有序的，目的是为了找回小于给定值的最大值）
					nextVal = values[i];
					return has;
				}
			}
			// 没有找到大于给定值的最小值，则返回最小值
			nextVal = values[values.Count - 1];
			return has;
		}

		/// <summary>
		/// 获取下一次执行时间
		/// </summary>
		/// <param name="months"></param>
		/// <param name="days"></param>
		/// <param name="currDate"></param>
		/// <returns></returns>
		private static DateTime GetRecentMonth(List<int> months, List<int> days, DateTime currDate)
		{
			int year = currDate.Year - 1;
			int month = -1, day = -1, maxDay = 31;
			for (var i = 0; i <= 8; i++)
			{
				year++;
				month = -1;
				day = -1;
				maxDay = 31;
				foreach (var m in months)
				{
					/// 先比较年份，年份大就没有什么好比的， 然后再比较月份。因此year最小值都等于currDate.Year，所以不加相等判断
					if (year > currDate.Year || m >= currDate.Month)
					{
						month = m;
						maxDay = GetLastDayByMonth(m, DateTime.IsLeapYear(year));
						foreach (var d in days)
						{
							/// 过滤非法日期，如果非润年是没有2月29，小月份没有31号等
							if (d > maxDay) continue;

							/// 比较当前循环到的日期是否比较指定的大，年份大或是月份大，则这个日期就是大，否则如果月份年份相同则比较天数
							if (year > currDate.Year || m > currDate.Month || m == currDate.Month && d >= currDate.Day)	// 月份相等就比较天
							{
								day = d;
								break;
							}
						}
						if (day != -1)
							break;
					}
				}
				if (month != -1 && day != -1)
					break;
			}
			return new DateTime(year, month, day);
		}
		#endregion

		/// <summary>
		/// 获取指定日期所在月份的最后一天
		/// </summary>
		/// <param name="dt"></param>
		/// <returns></returns>
		private static int GetLastDayByMonth(int month, bool isLeapYear)
		{
			switch (month)
			{
				case 4:
				case 6:
				case 9:
				case 11:
					return 30;
				case 2:
					return isLeapYear ? 29 : 28;
				default:
					return 31;
			}
		}
		#endregion

		#region 目标时间计算
		/// <summary>
		/// 获取下一个时间点
		/// </summary>
		/// <param name="dayStrategy">时间策略</param>
		/// <param name="currDate">当前时间</param>
		/// <param name="minData">最小时间</param>
		/// <param name="hasExpectData">是否有找到期望的时间</param>
		/// <returns></returns>
		private static TimeSpan GetNextTimeConfig(TimerStrategyByDay dayStrategy, DateTime currDate, out TimeSpan minData, out bool hasExpectData)
		{
			if (dayStrategy.TimePoints == null || dayStrategy.TimePoints.Length == 0)
				dayStrategy.TimePoints = new TimeSpan[] { new TimeSpan(0) };

			minData = new TimeSpan(23, 59, 59);		// 最小时间
			var minExpectData = TimeSpan.MaxValue;	// 大于当前时间的最小时间
			foreach (var t in dayStrategy.TimePoints)
			{
				if (currDate.TimeOfDay < t && minExpectData >= t)	// 找出比当前时间大的最小时间
					minExpectData = t;
				if (minData > t)	// 找出最小的一个时间，当前时间不参与运算
					minData = t;
			}
			hasExpectData = minExpectData != TimeSpan.MaxValue;
			if (hasExpectData)	// 如果找到比当前时间大的最小时间，则返回该时间
				return minExpectData;
			else
				return minData;
		}
		#endregion

		#endregion
	}
}
